///////////////////////////////////////////////////////////////////////////////
//
//  Copyright (C) 2007-2009 ILOG, S.A.
//  All Rights Reserved.
//  The following is ILOG Source Code.  No modifications may be made to the  
//  ILOG Source Code.  Any usage of the ILOG Source Code is subject to 
//  the terms and conditions of the ILOG End User License Agreement   
//  applicable to this ILOG software product.
//
///////////////////////////////////////////////////////////////////////////////

package ilog.gauges.gaugesClasses.rectangular
{
  import flash.display.DisplayObject;
  import flash.events.Event;
  import flash.events.MouseEvent;
  import flash.geom.Point;
  
  import ilog.utils.CSSUtil;
  
  import mx.effects.AnimateProperty;
  import mx.effects.easing.Quintic;
  import mx.events.EffectEvent;
  import mx.events.FlexEvent;
  import mx.events.SandboxMouseEvent;
  import mx.styles.CSSStyleDeclaration;

  /**
   * Dispatched when the value in the <code>RectangularValueRenderer</code> changes
   * through user input.
   * This event does not occur if you use data binding or 
   * ActionScript code to change the value.
   *
   * <p>Although the default value of the <code>Event.bubbles</code> property 
   * is <code>true</code>, this control dispatches the event with 
   * the <code>Event.bubbles</code> property set to <code>false</code>.</p>
   *
   * @eventType flash.events.Event.CHANGE
   */
  [Event(name="change", type="flash.events.Event")]
  
  /**
   * The length in milliseconds 
   * of a transition when changing the <code>value</code>. 
   * 
   * @default 500
   */
  [Style(name="animationDuration", type="Number", format="Time", inherit="no")]  

  /**
   * The easing function for the animation.
   * The easing function is used to interpolate when animating
   * the value on value changed.
   * A trivial easing function does linear interpolation.
   * More sophisticated easing functions create the illusion of
   * acceleration and deceleration, which makes the animation seem
   * more natural.
   * 
   * @see mx.effects.Tween#easingFunction
   * 
   * @default Quintic.easeInOut
   */
  [Style(name="easingFunction", type="Function", inherit="no")]   

  /**
   * The <code>RectangularValueRenderer</code> is the base class for visual 
   * rectangular elements that render a value relative to a scale. 
   *  
   * @mxml
   *  
   * <p>The <code>&lt;RectangularValueRenderer&gt;</code> tag inherits all the 
   * properties of its parent classes and adds the following properties:</p>
   *  
   * <pre>
   * &lt;RectangularValueRenderer
   *   <strong>Properties</strong>
   *   value="<i>null</i>"
   *   transitionPosition="NaN"
   *   editable="false|true"
   *   editMode="discrete|continuous"
   *   mouseMode="area|shape"
   *   
   *   <strong>Styles</strong>
   *   animationDuration="<i>500</i>"
   *   easingFunction="<i>Quintic.easeInOut</i>"
   *  /&gt;
   * </pre>
   * 
   * @see ilog.gauges.gaugesClasses.rectangular.IRectangularScale
   * @see ilog.gauges.gaugesClasses.rectangular.RectangularGaugeElement
   */   
  public class RectangularValueRenderer extends RectangularScaleRelatedRenderer
  {
    private static var stylesInited:Boolean = initStyles(); 

    private static function initStyles():Boolean {
      var nStyle:CSSStyleDeclaration =
        CSSUtil.createSelector("RectangularValueRenderer");   
            
      nStyle.defaultFactory = function():void  {
        this.animationDuration = 500;
        this.easingFunction = Quintic.easeInOut;
      }

      return true;
    }    
   
    private var _animateProperty:AnimateProperty;
    private var _down:Number;
    private var _keepTransition:Boolean = false;
    private var _preventAnimation:Boolean = false;

    /**
     * Constructor.
     */
    public function RectangularValueRenderer()
    {
    }
    
    private var _value:Object = null;
    
    [Bindable("change")]
    [Bindable("valueCommit")]
    [Inspectable(category="Gauge")]
    
    /**
     * The value rendered by the element. This can change either through user
     * input, if <code>editable</code> is <code>true</code>, or programmatically
     * by using the property setter.
     * 
     * @default null
     * @see #editable
     * @see #liveDragging
     */
    public function get value():Object
    {
      return _value;
    }

    /**
     * @private
     */
    public function set value(val:Object):void
    {
      // if the value has not change *and* we do not need to override
      // an existing transitionPosition let's do nothing
      if (_value == val && isNaN(_transitionPosition))
        return;
        
      if (val != null && createTween() != null && scale != null && 
          _value != null && !_preventAnimation) {
        animateTo(scale.positionForValue(val));
      }    
      
      _value = val;      
      invalidateDisplayList();
      dispatchEvent(new FlexEvent(FlexEvent.VALUE_COMMIT));
    }
    
    private function animateTo(toPosition:Number):void
    {
      var fromPosition:Number;
      if (_animateProperty.isPlaying) {
        fromPosition = transitionPosition;
        _animateProperty.stop();
      } else if (!isNaN(transitionPosition)) {
        fromPosition = transitionPosition;
      } else {
        fromPosition = scale.positionForValue(_value);
      }
      _animateProperty.fromValue = fromPosition;
      _animateProperty.toValue = toPosition;
      _animateProperty.play(); 
    }
    
    private var _transitionPosition:Number = NaN;
    
    [Inspectable(environment="none")]
    [Bindable("transitionPositionChanged")]
    
    /**
     * The position angle used during animation and interaction. It is set to 
     * <code>NaN</code> when the element is not animated or the user is not 
     * interacting. This property must be used by subclasses to temporarily update
     * their display during animation or interaction. Subclasses typically
     * do:
     * <pre>
     * protected override function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
     * {
     *    var position:Number;
     *    if (isNaN(transitionPosition)) {
     *       // use the value
     *       position = scale.positionForValue(value);
     *    } else {
     *       position = transitionPosition;
     *    }
     *    // draw something depending on position
     * } 
     * </pre>
     */       
    public function get transitionPosition():Number
    {
      return _transitionPosition;
    }
    
    /**
     * @private
     */
    public function set transitionPosition(val:Number):void 
    {
      _transitionPosition = val; 
      invalidateDisplayList();
      dispatchEvent(new Event("transitionPositionChanged"));
    }
       
    private var _editable:Boolean = false;
    
    [Inspectable(category="Gauge")]    

    /**
     * Indicates whether the user is allowed to edit the value of the element.
     * If <code>true</code>, the user can drag the element to change its value.
     *
     * @default false
     */ 
    public function get editable():Boolean
    {
      return _editable;
    }
    
    /**
     * @private
     */
    public function set editable(value:Boolean):void
    {
      if (_editable == value)
        return;
      _editable = value;
      if (_editable) {
        setUpShield();
      } else {
        tearDownShield();
      }
      // draw a hit shield if area mode is on, remove it otherwise
      invalidateDisplayList();      
    }       
    
    // we need a shield because we might have some foreground
    // skin on top of us that is catching events, so we need to catch
    // them a bit heigher
    private function setUpShield():void
    {
      buttonMode = true;
      useHandCursor = true;       
      addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);        
    } 
    
    private function tearDownShield():void
    {
      removeEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
      buttonMode = false;
      useHandCursor = false; 
    }
       
    [Inspectable(category="Gauge", defaultValue="false")]

    /**
     * Specifies whether live dragging is enabled for the element.
     * If <code>false</code>, the <code>value</code>
     * property is changed and a <code>valueChanged</code> 
     * event is dispatched when the user stops moving the element.
     * If <code>true</code>,  the <code>value</code>
     * property is changed and a <code>valueChanged</code>
     * event is dispatched continuously as the user moves the element.
     *
     * @default false
     */
    public var liveDragging:Boolean = false;
    
    private var _editMode:String = "discrete";
    
    [Inspectable(category="Gauge", enumeration="continuous,discrete")]

    /**
     * The interaction mode when editing the value. If <code>"discrete"</code>, 
     * the value renderer snaps to the scale accepted values
     * each time the user moves the mouse, otherwise the value renderer moves continuously.
     *  
     * @default "discrete"
     * @see ilog.gauges.gaugesClasses.LinearScale#snapInterval
     */     
    public function get editMode():String
    {
      return _editMode;
    }

    /**
     * @private
     */
    public function set editMode(value:String):void
    {
      _editMode = value;
    }    
    
    /**
     *  @private
     */
    private var _mouseMode:String = "area";
        
    [Inspectable(category="Gauge", enumeration="shape,area")]
    
    /**
     * Specifies whether only the shape of the renderer reacts to mouse interaction. If 
     * <code>"area"</code> is set, mouse interaction is enabled in the bounds of the
     * value renderer. If <code>"shape"</code> is set, mouse interaction is enabled 
     * on the shape of the value renderer.
     * This property has no effect if <code>editable</code> is set to <code>false</code>.
     * 
     * @default "area"
     */     
    public function get mouseMode():String
    {
      return _mouseMode;
    }

    /**
     * @private
     */
    public function set mouseMode(value:String):void
    {
      _mouseMode = value;
      invalidateDisplayList();
    } 
    
    private var _delegate:RectangularGaugeElement;
    
    [Inspectable(category="Gauge")]
    
    /**
     * A delegate <code>RectangularGaugeElement</code> which will be 
     * in charge of rendering the value of the <code>RectangularValueRenderer</code>.
     * This is useful when you want to use a <code>RectangularGaugeElement</code>
     * that does not inherit from <code>RectangularValueRenderer</code> as the one
     * in charge of rendering the value.
     * 
     * @since ILOG Elixir 2.0
     * @ilog.reviewed SH 5 May 08
     */     
    public function get delegate():RectangularGaugeElement
    {
      return _delegate;
    }

    /**
     * @private
     */
    public function set delegate(value:RectangularGaugeElement):void
    {
      _delegate = value;
      invalidateDisplayList();
    }       
    
    /**
     * @private
     */
    protected override function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
    {
      super.updateDisplayList(unscaledWidth, unscaledHeight);
      // in area mode we need some dummy drawing so that we are sure to hitTest
      if (editable && mouseMode == "area") {
        graphics.beginFill(0x000000, 0);
        graphics.drawRect(0, 0, unscaledWidth, unscaledHeight);
        graphics.endFill();
      }
      if (delegate) {
        delegate.invalidateDisplayList();
      }
    }
 
    private function createTween():AnimateProperty
    {
      var animationDuration:Number = getStyle("animationDuration");
      if (animationDuration > 0) {
        if (_animateProperty == null) { 
          _animateProperty = new AnimateProperty(this);
          _animateProperty.property = "transitionPosition";
          _animateProperty.addEventListener(EffectEvent.EFFECT_END, endTween);
        }
        setupTween(animationDuration, getStyle("easingFunction"));
      } else {
        // reset it
        _animateProperty = null;
      }
      return _animateProperty; 
    }
    
    private function setupTween(animationDuration:Number, easingFunction:Function):void
    {
      if (_animateProperty != null) {
        _animateProperty.duration = animationDuration;
        _animateProperty.easingFunction = easingFunction;
      }
    }
    
    private function endTween(e:Event):void
    {
      if (!_keepTransition) {
        _transitionPosition = NaN;     
      }
    }
    
    private function mouseDownHandler(event:MouseEvent):void
    {     
      if (!enabled)
        return;

      var sandboxRoot:DisplayObject = systemManager.getSandboxRoot();

      // Listener to catch mouse up all up events on the systemManager. We need
      // catpure = true to be sure to get them all.
      sandboxRoot.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler, true);      
      // The above listener doesn't give use the up events outside the plugin
      // area so we also need a listener on the stage. Unfortunately puting capture =
      // true on that one would prevent us to get outside event! Thus the above
      // listener is still needed.mouseUp
      sandboxRoot.addEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE, mouseUpSomewhereHandler);
      // Listener to catch mouse up all move events on the systemManager. We need
      // catpure = true to be sure to get them all.
      sandboxRoot.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler, true);       
      // The above listener doesn't give use the move events outside the plugin
      // area so we also need a listener on the stage. Unfortunately puting capture =
      // true on that one would prevent us to get outside event! Thus the above
      // listener is still needed.
      // We also need a specific listener to make sure we do not get twice
      // the move events when both systemManager & stage listeners are called.
      sandboxRoot.addEventListener(SandboxMouseEvent.MOUSE_MOVE_SOMEWHERE, mouseMoveSomewhereHandler); 
      var local:Point = globalToLocal(new Point(event.stageX, event.stageY));       
      if (gauge.direction == "horizontal")
        _down = local.x;
      else
        _down = local.y;
      updateValue(local.x, local.y, false, true);
    }

    private function mouseMoveSomewhereHandler(event:MouseEvent):void  
    {
      var p:Point = new Point(mouseX, mouseY);
      p = localToGlobal(p);
      mouseMoveHandlerImpl(p.x, p.y);
    }
    
    private function mouseMoveHandler(event:MouseEvent):void  
    {
      mouseMoveHandlerImpl(event.stageX, event.stageY);
    }

    private function mouseMoveHandlerImpl(stageX:Number, stageY:Number):void
    {
      if (!enabled)
        return;
      var local:Point = globalToLocal(new Point(stageX, stageY));
      if (gauge.direction == "horizontal") {
        if (!isNaN(_down) && Math.abs(_down - local.x) < 2)
          return; 
        else 
          _down = NaN;
      } else {
        if (!isNaN(_down) && Math.abs(_down - local.y) < 2)
          return;
        else
         _down = NaN;
      }
      updateValue(local.x, local.y, false, false);
      
    }      

    /**
     *  @private
     */
    private function mouseUpSomewhereHandler(e:SandboxMouseEvent):void
    {
      var p:Point = new Point(mouseX, mouseY);
      p = localToGlobal(p);
      mouseUpHandlerImpl(p.x, p.y);  
         
    }

    /**
     *  @private
     */
    private function mouseUpHandler(e:MouseEvent):void
    {
      mouseUpHandlerImpl(e.stageX, e.stageY); 
      
    }

    /**
     *  @private
     */
    public function mouseUpHandlerImpl(stageX:Number, stageY:Number):void  
    {
      if (!enabled)
       return;      
       
      var sandboxRoot:DisplayObject = systemManager.getSandboxRoot();
      // Remove temporarly listeners.
      sandboxRoot.removeEventListener(MouseEvent.MOUSE_UP, mouseUpHandler, true);
      sandboxRoot.removeEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE, mouseUpSomewhereHandler);
      sandboxRoot.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler, true);
      sandboxRoot.removeEventListener(SandboxMouseEvent.MOUSE_MOVE_SOMEWHERE, mouseMoveSomewhereHandler);  

      var local:Point = globalToLocal(new Point(stageX, stageY));
      updateValue(local.x, local.y, true, true);    
        
    }

    /**
     *  @private
     */
    private function updateValue(x:Number, y:Number, forceCommit:Boolean, animate:Boolean):void
    {
      var position:Number = (gauge.direction == "horizontal") ? x : unscaledHeight - y;
      // if we are not in liveDragging, or if we require continuous drawing
      // we *must* go through transitionPosition and not rely on the value
      // to redraw the value renderer. Of course not needed if we are in
      // forceCommit
      if ((editMode == "continuous" || !liveDragging) && !forceCommit) {
        _keepTransition = true;
        // just visually drag, this is not snapping if any snapping
        // is required => do first a snap trick
        if (animate && createTween() != null)
          animateTo(snapPosition(position))
        else {
          // if an animation was going on let's stop it
          if (_animateProperty != null && _animateProperty.isPlaying) {
            _animateProperty.stop();           
          }
          transitionPosition = snapPosition(position);
        }
      }      
      var newvalue:Object = scale.valueForPosition(position);
      if (forceCommit || liveDragging && newvalue != value) {        
        if (forceCommit) {
          // we actually commit a value, so we do not need anymore transition position
          _keepTransition = false;
        }
        // if we are not asked to animate or if the animation already occured
        // in continuous drag above let's temporarily cancel animation
        if (!animate || ((editMode == "continuous" || !liveDragging) && !forceCommit)) {
          _preventAnimation = true;     
          if (!animate && _animateProperty != null && _animateProperty.isPlaying) {
            _animateProperty.stop();
          }   
        }
        // actually set the value, snapping will be done if needed
        value = newvalue;
        if (!animate || ((editMode == "continuous" || !liveDragging) && !forceCommit)) {
          _preventAnimation = false;
        }
        if (forceCommit) {
          // make sure that after a force commit we have no more transition
          // value pending
          if (_animateProperty == null || !_animateProperty.isPlaying) {
            _transitionPosition = NaN;
          }            
        }        
        dispatchEvent(new Event(Event.CHANGE));        
      }               
    }
   
    /**
     * Snaps the value if needed.
     * 
     * @param position The position to snap to.
     * 
     * @return The snapped position.
     * 
     * @see #liveDragging
     */
    private function snapPosition(position:Number):Number
    {
      if (editMode == "discrete") {
        return scale.positionForValue(scale.valueForPosition(position));
      } else {
        // do not snap if continuous mode, but still give it some limits
        if (scale.positionInRange(position))
          return position;
        else {
          if (position < scale.offset)
            return scale.offset;
          else
            return scale.offset + scale.length;
        }
      }    
    }
  }
}
